﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;
using System.Web.Routing;
using System.Reflection;
using SimpleRest.Core.Routing;
using SimpleRest.Core.Exceptions;

namespace SimpleRest.Core.Routing.Handlers
{
    /// <summary>
    /// The actual operation http handler that gets executed for a registered operation route.
    /// </summary>
    public class OperationHttpHandler : IHttpHandler
    {
        /// <summary>
        /// The actual request context for the associated request.
        /// </summary>
        private readonly RequestContext RequestContext;
        /// <summary>
        /// The associated MethodInfo object for the method that this operation is targeting.
        /// </summary>
        private readonly MethodInfo TargetMethod;
        /// <summary>
        /// The service configuration that this request is to adhere to.
        /// </summary>
        private readonly Configuration Configuration;
        /// <summary>
        /// The actual pipeline instance that each request is pushed through for this http handler.
        /// </summary>
        private readonly Pipeline OperationPipeline;
        /// <summary>
        /// The associated operation route tied to this http handler.
        /// </summary>
        public OperationRoute AssociatedRoute { get; private set; }
        private readonly IHttpRequestMessage HttpRequestMessage;
        
        public OperationHttpHandler(Pipeline operationPipeline, RequestContext requestContext, MethodInfo targetMethod, Configuration configuration, IHttpRequestMessage httpRequestMessage)
        {
            this.RequestContext = requestContext;
            this.Configuration = configuration;
            this.TargetMethod = targetMethod;
            this.OperationPipeline = operationPipeline;
            
            this.AssociatedRoute = RouteUtility.GetRegisteredOperations().FirstOrDefault(d => d.TargetMethod == this.TargetMethod);
            this.HttpRequestMessage = httpRequestMessage;
        }

        /// <summary>
        /// Tells the ASP.NET pipeline to always create a new instance of this http handler for each new request coming in.
        /// </summary>
        public bool IsReusable
        {
            get { return false; }
        }

        /// <summary>
        /// The code executed for each successfully routed request.  This is where the REST pipeline logic begins.
        /// </summary>
        /// <param name="context"></param>
        public void ProcessRequest(HttpContext context)
        {
            IHttpResponseMessage httpResponseMessage = new HttpResponseMessage();
            IMediaTypeFormatter responseMediaTypeFormatter = null;

            try
            {
                responseMediaTypeFormatter = Pipeline.GetResponseMediaTypeFormatter(HttpRequestMessage, this.Configuration);
                if (responseMediaTypeFormatter == null || !responseMediaTypeFormatter.CanSerialize(this.TargetMethod.ReturnType))
                {
                    context.Response.StatusCode = (int)System.Net.HttpStatusCode.UnsupportedMediaType;
                    context.Response.End();
                    return;
                }
            }
            catch (Exception ex)
            {
                this.Configuration.ErrorHandler(new ErrorHandlerEventArgs(ex, this.OperationPipeline.ServiceType, HttpRequestMessage, httpResponseMessage));
            }

            if (responseMediaTypeFormatter != null)
            {
                RouteInformation routeInformation = new RouteInformation(this.AssociatedRoute.Route.Url);
                routeInformation.RouteData = this.RequestContext.RouteData.Values;
                httpResponseMessage = this.OperationPipeline.ExecuteOperation(this.TargetMethod, HttpRequestMessage, this.Configuration);
            }
            else
            {
                context.Response.StatusCode = (int)System.Net.HttpStatusCode.UnsupportedMediaType;
                context.Response.End();
                return;
            }

            if (httpResponseMessage != null)
            {
                context.Response.StatusCode = (int)httpResponseMessage.StatusCode;

                if (responseMediaTypeFormatter != null && httpResponseMessage.Content != null)
                {
                    var stream = responseMediaTypeFormatter.Serialize(httpResponseMessage.Content);
                    string responseText = System.Text.Encoding.UTF8.GetString(ReadToEnd(stream));
                    context.Response.Write(responseText);
                    stream.Close();
                }

                if (httpResponseMessage.ResponseHeaders != null)
                    foreach (var header in httpResponseMessage.ResponseHeaders)
                        context.Response.AddHeader(header.Key, header.Value);
            }
        }

        /// <summary>
        /// Helper method to turn a stream into a byte array.  Pulled this thing off of stack overflow and it seems to work well.
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        private static byte[] ReadToEnd(System.IO.Stream stream)
        {
            long originalPosition = 0;

            if (stream.CanSeek)
            {
                originalPosition = stream.Position;
                stream.Position = 0;
            }

            try
            {
                byte[] readBuffer = new byte[stream.Length];

                int totalBytesRead = 0;
                int bytesRead;

                while ((bytesRead = stream.Read(readBuffer, totalBytesRead, readBuffer.Length - totalBytesRead)) > 0)
                {
                    totalBytesRead += bytesRead;

                    if (totalBytesRead == readBuffer.Length)
                    {
                        int nextByte = stream.ReadByte();
                        if (nextByte != -1)
                        {
                            byte[] temp = new byte[readBuffer.Length * 2];
                            Buffer.BlockCopy(readBuffer, 0, temp, 0, readBuffer.Length);
                            Buffer.SetByte(temp, totalBytesRead, (byte)nextByte);
                            readBuffer = temp;
                            totalBytesRead++;
                        }
                    }
                }

                byte[] buffer = readBuffer;
                if (readBuffer.Length != totalBytesRead)
                {
                    buffer = new byte[totalBytesRead];
                    Buffer.BlockCopy(readBuffer, 0, buffer, 0, totalBytesRead);
                }
                return buffer;
            }
            finally
            {
                if (stream.CanSeek)
                {
                    stream.Position = originalPosition;
                }
            }
        }
    }
}
